package org.jfxvnc.net.rfb;

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import org.jfxvnc.net.rfb.codec.ProtocolInitializer;
import org.jfxvnc.net.rfb.render.DefaultProtocolConfiguration;
import org.jfxvnc.net.rfb.render.ProtocolConfiguration;
import org.jfxvnc.net.rfb.render.RenderProtocol;
import org.slf4j.LoggerFactory;

import javax.annotation.PreDestroy;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

public class VncConnection {

    private final static org.slf4j.Logger logger = LoggerFactory.getLogger(VncConnection.class);

    private static final int DEF_CONNECT_PORT = 5900;
    private static final int DEF_LISTENING_PORT = 5500;

    private final ProtocolConfiguration config;

    private final ReadOnlyBooleanWrapper connected = new ReadOnlyBooleanWrapper(false);
    private final ReadOnlyBooleanWrapper connecting = new ReadOnlyBooleanWrapper(false);

    private final ThreadFactory executor;

    private NioEventLoopGroup workerGroup;

    private RenderProtocol render;

    private NioEventLoopGroup bossGroup;

    public VncConnection() {
        this(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("vnc-connection-" + t.getId());
                t.setDaemon(true);
                return t;
            }
        });
    }

    public VncConnection(ThreadFactory executor) {
        this.executor = Objects.requireNonNull(executor);
        this.config = new DefaultProtocolConfiguration();
    }

    public ProtocolConfiguration getConfiguration() {
        return this.config;
    }

    public void setRenderProtocol(RenderProtocol render) {
        this.render = Objects.requireNonNull(render);
    }

    public void addFaultListener(Consumer<Throwable> l) {
    }

    public void removeFaultListener(Consumer<Throwable> l) {
    }

    public CompletableFuture<VncConnection> connect() {
        shutdown();
        CompletableFuture<VncConnection> future = new CompletableFuture<>();

        workerGroup = new NioEventLoopGroup(2, executor);

        connecting.set(true);

        String host = config.hostProperty().get();
        int port = config.portProperty().get() > 0 ? config.portProperty().get() : DEF_CONNECT_PORT;

        Bootstrap b = new Bootstrap();
        b.group(workerGroup);
        b.channel(NioSocketChannel.class);
        b.option(ChannelOption.SO_KEEPALIVE, true);
        b.option(ChannelOption.TCP_NODELAY, true);
        b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);

        ChannelHandler handler = this.initHandler();
        b.handler(handler);
        // b.handler(new ProtocolInitializer(render, config));

        logger.debug("try to connect to {}:{}", host, port);
        b.connect(host, port).addListener((ChannelFuture in) -> {
            connecting.set(false);
            connected.set(in.isSuccess());
            if (!in.isSuccess()) {
                future.completeExceptionally(in.cause());
                return;
            }
            future.complete(this);
        });

        return future;
    }

    public CompletableFuture<VncConnection> startListeningMode() {

        shutdown();
        CompletableFuture<VncConnection> future = new CompletableFuture<>();
        connected.set(true);

        bossGroup = new NioEventLoopGroup(1, executor);
        workerGroup = new NioEventLoopGroup(2, executor);

        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100);

        ChannelHandler handler = this.initHandler();
        b.childHandler(handler);
        // b.childHandler(new ProtocolInitializer(render, config));
        int port = config.listeningPortProperty().get() > 0 ? config.listeningPortProperty().get() : DEF_LISTENING_PORT;

        b.bind(port).addListener(l -> {
            logger.debug("wait for incoming connection request on port: {}..", port);
            connected.set(l.isSuccess());
        }).addListener((ChannelFuture in) -> {
            if (!in.isSuccess()) {
                connecting.set(false);
                connected.set(false);
                future.completeExceptionally(in.cause());
            }
            future.complete(this);
        });

        return future;

    }

    @PreDestroy
    private void shutdown() {
        if (workerGroup != null && !workerGroup.isTerminated()) {
            workerGroup.shutdownGracefully(2, 5, TimeUnit.SECONDS);
        }
        if (bossGroup != null && !bossGroup.isTerminated()) {
            bossGroup.shutdownGracefully(2, 5, TimeUnit.SECONDS);
        }
    }

    public CompletableFuture<VncConnection> disconnect() {

        if (!isConnected()) {
            return CompletableFuture.completedFuture(this);
        }
        connecting.set(false);
        connected.set(false);
        return CompletableFuture.supplyAsync(() -> {
            shutdown();
            return this;
        });

    }

    public boolean isConnected() {
        return connected.get();
    }

    public boolean isConnecting() {
        return connecting.get();
    }

    public ReadOnlyBooleanProperty connectedProperty() {
        return connected.getReadOnlyProperty();
    }

    public ReadOnlyBooleanProperty connectingProperty() {
        return connecting.getReadOnlyProperty();
    }

    protected ChannelHandler initHandler() {
        return new ProtocolInitializer(render, config);
    }

    public RenderProtocol getRender() {
        return render;
    }

    public ProtocolConfiguration getConfig() {
        return config;
    }
}
